{ pkgs }:
let
  inherit (pkgs) lib formats;

  # merging allows us to add metadata to the input
  # this makes error messages more readable during development
  mergeInput =
    name: format: input:
    format.type.merge
      [ ]
      [
        {
          # explicitly throw here to trigger the code path that prints the error message for users
          value =
            lib.throwIfNot (format.type.check input)
              (builtins.trace input "definition does not pass the type's check function")
              input;
          # inject the name
          file = "format-test-${name}";
        }
      ];

  # run a diff between expected and real output
  runDiff =
    name: drv: expected:
    pkgs.runCommand name
      {
        passAsFile = [ "expected" ];
        inherit expected drv;
      }
      ''
        if diff -u "$expectedPath" "$drv"; then
          touch "$out"
        else
          echo
          echo "Got different values than expected; diff above."
          exit 1
        fi
      '';

  # use this to check for proper serialization
  # in practice you do not have to supply the name parameter as this one will be added by runBuildTests
  shouldPass =
    {
      format,
      input,
      expected,
    }:
    name: {
      name = "pass-${name}";
      path = runDiff "test-format-${name}" (format.generate "test-format-${name}" (
        mergeInput name format input
      )) expected;
    };

  # use this function to assert that a type check must fail
  # in practice you do not have to supply the name parameter as this one will be added by runBuildTests
  # note that as per 352e7d330a26 and 352e7d330a26 the type checking of attrsets and lists are not strict
  # this means that the code below needs to properly merge the module type definition and also evaluate the (lazy) return value
  shouldFail =
    { format, input }:
    name:
    let
      # trigger a deep type check using the module system
      typeCheck = lib.modules.mergeDefinitions [ "tests" name ] format.type [
        {
          file = "format-test-${name}";
          value = input;
        }
      ];
      # actually use the return value to trigger the evaluation
      eval = builtins.tryEval (typeCheck.mergedValue == input);
      # the check failing is what we want, so don't do anything here
      typeFails = pkgs.runCommand "test-format-${name}" { } "touch $out";
      # bail with some verbose information in case the type check passes
      typeSucceeds =
        pkgs.runCommand "test-format-${name}"
          {
            passAsFile = [ "inputText" ];
            testName = name;
            # this will fail if the input contains functions as values
            # however that should get caught by the type check already
            inputText = builtins.toJSON input;
          }
          ''
            echo "Type check $testName passed when it shouldn't."
            echo "The following data was used as input:"
            echo
            cat "$inputTextPath"
            exit 1
          '';
    in
    {
      name = "fail-${name}";
      path = if eval.success then typeSucceeds else typeFails;
    };

  # this function creates a linkFarm for all the tests below such that the results are easily visible in the filesystem after a build
  # the parameters are an attrset of name: test pairs where the name is automatically passed to the test
  # the test therefore is an invocation of ShouldPass or shouldFail with the attrset parameters but *not* the name (which this adds for convenience)
  runBuildTests = (lib.flip lib.pipe) [
    (lib.mapAttrsToList (name: value: value name))
    (pkgs.linkFarm "nixpkgs-pkgs-lib-format-tests")
  ];

in
runBuildTests {

  jsonAtoms = shouldPass {
    format = formats.json { };
    input = {
      null = null;
      false = false;
      true = true;
      int = 10;
      float = 3.141;
      str = "foo";
      attrs.foo = null;
      list = [
        null
        null
      ];
      path = ./testfile;
    };
    expected = ''
      {
        "attrs": {
          "foo": null
        },
        "false": false,
        "float": 3.141,
        "int": 10,
        "list": [
          null,
          null
        ],
        "null": null,
        "path": "${./testfile}",
        "str": "foo",
        "true": true
      }
    '';
  };

  yaml_1_1Atoms = shouldPass {
    format = formats.yaml_1_1 { };
    input = {
      null = null;
      false = false;
      true = true;
      float = 3.141;
      str = "foo";
      attrs.foo = null;
      list = [
        null
        null
      ];
      path = ./testfile;
      no = "no";
      time = "22:30:00";
    };
    expected = ''
      attrs:
        foo: null
      'false': false
      float: 3.141
      list:
      - null
      - null
      'no': 'no'
      'null': null
      path: ${./testfile}
      str: foo
      time: '22:30:00'
      'true': true
    '';
  };

  yaml_1_2Atoms = shouldPass {
    format = formats.yaml_1_2 { };
    input = {
      null = null;
      false = false;
      true = true;
      float = 3.141;
      str = "foo";
      attrs.foo = null;
      list = [
        null
        null
      ];
      path = ./testfile;
      no = "no";
      time = "22:30:00";
    };
    expected = ''
      attrs:
        foo: null
      'false': false
      float: 3.141
      list:
      - null
      - null
      no: no
      'null': null
      path: ${./testfile}
      str: foo
      time: 22:30:00
      'true': true
    '';
  };

  iniAtoms = shouldPass {
    format = formats.ini { };
    input = {
      foo = {
        bool = true;
        int = 10;
        float = 3.141;
        str = "string";
      };
    };
    expected = ''
      [foo]
      bool=true
      float=3.141000
      int=10
      str=string
    '';
  };

  iniInvalidAtom = shouldFail {
    format = formats.ini { };
    input = {
      foo = {
        function = _: 1;
      };
    };
  };

  iniDuplicateKeysWithoutList = shouldFail {
    format = formats.ini { };
    input = {
      foo = {
        bar = [
          null
          true
          "test"
          1.2
          10
        ];
        baz = false;
        qux = "qux";
      };
    };
  };

  iniDuplicateKeys = shouldPass {
    format = formats.ini { listsAsDuplicateKeys = true; };
    input = {
      foo = {
        bar = [
          null
          true
          "test"
          1.2
          10
        ];
        baz = false;
        qux = "qux";
      };
    };
    expected = ''
      [foo]
      bar=null
      bar=true
      bar=test
      bar=1.200000
      bar=10
      baz=false
      qux=qux
    '';
  };

  iniListToValue = shouldPass {
    format = formats.ini {
      listToValue = lib.concatMapStringsSep ", " (lib.generators.mkValueStringDefault { });
    };
    input = {
      foo = {
        bar = [
          null
          true
          "test"
          1.2
          10
        ];
        baz = false;
        qux = "qux";
      };
    };
    expected = ''
      [foo]
      bar=null, true, test, 1.200000, 10
      baz=false
      qux=qux
    '';
  };

  iniCoercedDuplicateKeys = shouldPass rec {
    format = formats.ini {
      listsAsDuplicateKeys = true;
      atomsCoercedToLists = true;
    };
    input =
      format.type.merge
        [ ]
        [
          {
            file = "format-test-inner-iniCoercedDuplicateKeys";
            value = {
              foo = {
                bar = 1;
              };
            };
          }
          {
            file = "format-test-inner-iniCoercedDuplicateKeys";
            value = {
              foo = {
                bar = 2;
              };
            };
          }
        ];
    expected = ''
      [foo]
      bar=1
      bar=2
    '';
  };

  iniCoercedListToValue = shouldPass rec {
    format = formats.ini {
      listToValue = lib.concatMapStringsSep ", " (lib.generators.mkValueStringDefault { });
      atomsCoercedToLists = true;
    };
    input =
      format.type.merge
        [ ]
        [
          {
            file = "format-test-inner-iniCoercedListToValue";
            value = {
              foo = {
                bar = 1;
              };
            };
          }
          {
            file = "format-test-inner-iniCoercedListToValue";
            value = {
              foo = {
                bar = 2;
              };
            };
          }
        ];
    expected = ''
      [foo]
      bar=1, 2
    '';
  };

  iniCoercedNoLists = shouldFail {
    format = formats.ini { atomsCoercedToLists = true; };
    input = {
      foo = {
        bar = 1;
      };
    };
  };

  iniNoCoercedNoLists = shouldFail {
    format = formats.ini { atomsCoercedToLists = false; };
    input = {
      foo = {
        bar = 1;
      };
    };
  };

  iniWithGlobalNoSections = shouldPass {
    format = formats.iniWithGlobalSection { };
    input = { };
    expected = "";
  };

  iniWithGlobalOnlySections = shouldPass {
    format = formats.iniWithGlobalSection { };
    input = {
      sections = {
        foo = {
          bar = "baz";
        };
      };
    };
    expected = ''
      [foo]
      bar=baz
    '';
  };

  iniWithGlobalOnlyGlobal = shouldPass {
    format = formats.iniWithGlobalSection { };
    input = {
      globalSection = {
        bar = "baz";
      };
    };
    expected = ''
      bar=baz

    '';
  };

  iniWithGlobalWrongSections = shouldFail {
    format = formats.iniWithGlobalSection { };
    input = {
      foo = { };
    };
  };

  iniWithGlobalEverything = shouldPass {
    format = formats.iniWithGlobalSection { };
    input = {
      globalSection = {
        bar = true;
      };
      sections = {
        foo = {
          bool = true;
          int = 10;
          float = 3.141;
          str = "string";
        };
      };
    };
    expected = ''
      bar=true

      [foo]
      bool=true
      float=3.141000
      int=10
      str=string
    '';
  };

  iniWithGlobalListToValue = shouldPass {
    format = formats.iniWithGlobalSection {
      listToValue = lib.concatMapStringsSep ", " (lib.generators.mkValueStringDefault { });
    };
    input = {
      globalSection = {
        bar = [
          null
          true
          "test"
          1.2
          10
        ];
        baz = false;
        qux = "qux";
      };
      sections = {
        foo = {
          bar = [
            null
            true
            "test"
            1.2
            10
          ];
          baz = false;
          qux = "qux";
        };
      };
    };
    expected = ''
      bar=null, true, test, 1.200000, 10
      baz=false
      qux=qux

      [foo]
      bar=null, true, test, 1.200000, 10
      baz=false
      qux=qux
    '';
  };

  iniWithGlobalCoercedDuplicateKeys = shouldPass rec {
    format = formats.iniWithGlobalSection {
      listsAsDuplicateKeys = true;
      atomsCoercedToLists = true;
    };
    input =
      format.type.merge
        [ ]
        [
          {
            file = "format-test-inner-iniWithGlobalCoercedDuplicateKeys";
            value = {
              globalSection = {
                baz = 4;
              };
              sections = {
                foo = {
                  bar = 1;
                };
              };
            };
          }
          {
            file = "format-test-inner-iniWithGlobalCoercedDuplicateKeys";
            value = {
              globalSection = {
                baz = 3;
              };
              sections = {
                foo = {
                  bar = 2;
                };
              };
            };
          }
        ];
    expected = ''
      baz=3
      baz=4

      [foo]
      bar=2
      bar=1
    '';
  };

  iniWithGlobalCoercedListToValue = shouldPass rec {
    format = formats.iniWithGlobalSection {
      listToValue = lib.concatMapStringsSep ", " (lib.generators.mkValueStringDefault { });
      atomsCoercedToLists = true;
    };
    input =
      format.type.merge
        [ ]
        [
          {
            file = "format-test-inner-iniWithGlobalCoercedListToValue";
            value = {
              globalSection = {
                baz = 4;
              };
              sections = {
                foo = {
                  bar = 1;
                };
              };
            };
          }
          {
            file = "format-test-inner-iniWithGlobalCoercedListToValue";
            value = {
              globalSection = {
                baz = 3;
              };
              sections = {
                foo = {
                  bar = 2;
                };
              };
            };
          }
        ];
    expected = ''
      baz=3, 4

      [foo]
      bar=2, 1
    '';
  };

  iniWithGlobalCoercedNoLists = shouldFail {
    format = formats.iniWithGlobalSection { atomsCoercedToLists = true; };
    input = {
      globalSection = {
        baz = 4;
      };
      foo = {
        bar = 1;
      };
    };
  };

  iniWithGlobalNoCoercedNoLists = shouldFail {
    format = formats.iniWithGlobalSection { atomsCoercedToLists = false; };
    input = {
      globalSection = {
        baz = 4;
      };
      foo = {
        bar = 1;
      };
    };
  };

  keyValueAtoms = shouldPass {
    format = formats.keyValue { };
    input = {
      bool = true;
      int = 10;
      float = 3.141;
      str = "string";
    };
    expected = ''
      bool=true
      float=3.141000
      int=10
      str=string
    '';
  };

  keyValueDuplicateKeys = shouldPass {
    format = formats.keyValue { listsAsDuplicateKeys = true; };
    input = {
      bar = [
        null
        true
        "test"
        1.2
        10
      ];
      baz = false;
      qux = "qux";
    };
    expected = ''
      bar=null
      bar=true
      bar=test
      bar=1.200000
      bar=10
      baz=false
      qux=qux
    '';
  };

  keyValueListToValue = shouldPass {
    format = formats.keyValue {
      listToValue = lib.concatMapStringsSep ", " (lib.generators.mkValueStringDefault { });
    };
    input = {
      bar = [
        null
        true
        "test"
        1.2
        10
      ];
      baz = false;
      qux = "qux";
    };
    expected = ''
      bar=null, true, test, 1.200000, 10
      baz=false
      qux=qux
    '';
  };

  tomlAtoms = shouldPass {
    format = formats.toml { };
    input = {
      false = false;
      true = true;
      int = 10;
      float = 3.141;
      str = "foo";
      attrs.foo = "foo";
      list = [
        1
        2
      ];
      level1.level2.level3.level4 = "deep";
    };
    expected = ''
      false = false
      float = 3.141
      int = 10
      list = [1, 2]
      str = "foo"
      true = true

      [attrs]
      foo = "foo"

      [level1.level2.level3]
      level4 = "deep"
    '';
  };

  cdnAtoms = shouldPass {
    format = formats.cdn { };
    input = {
      null = null;
      false = false;
      true = true;
      int = 10;
      float = 3.141;
      str = "foo";
      attrs.foo = null;
      list = [
        1
        null
      ];
      path = ./testfile;
    };
    expected = ''
      attrs {
        "foo": null
      }
      "false": false
      "float": 3.141
      "int": 10
      list [
        1,
        null
      ]
      "null": null
      "path": "${./testfile}"
      "str": "foo"
      "true": true
    '';
  };

  # This test is responsible for
  #   1. testing type coercions
  #   2. providing a more readable example test
  # Whereas java-properties/default.nix tests the low level escaping, etc.
  javaProperties = shouldPass {
    format = formats.javaProperties { };
    input = {
      floaty = 3.1415;
      tautologies = true;
      contradictions = false;
      foo = "bar";
      # # Disallowed at eval time, because it's ambiguous:
      # # add to store or convert to string?
      # root = /root;
      "1" = 2;
      package = pkgs.hello;
      "ütf 8" = "dûh";
      # NB: Some editors (vscode) show this _whole_ line in right-to-left order
      "الجبر" = "أكثر من مجرد أرقام";
    };
    expected = ''
      # Generated with Nix

      1 = 2
      contradictions = false
      floaty = 3.141500
      foo = bar
      package = ${pkgs.hello}
      tautologies = true
      \u00fctf\ 8 = d\u00fbh
      \u0627\u0644\u062c\u0628\u0631 = \u0623\u0643\u062b\u0631 \u0645\u0646 \u0645\u062c\u0631\u062f \u0623\u0631\u0642\u0627\u0645
    '';
  };

  luaTable = shouldPass {
    format = formats.lua { };
    input = {
      null = null;
      false = false;
      true = true;
      int = 10;
      float = 3.141;
      str = "foo";
      attrs.foo = null;
      list = [
        null
        null
      ];
      path = ./testfile;
      inline = lib.mkLuaInline "hello('world')";
    };
    expected = ''
      return {
        ["attrs"] = {
          ["foo"] = nil,
        },
        ["false"] = false,
        ["float"] = 3.141,
        ["inline"] = (hello("world")),
        ["int"] = 10,
        ["list"] = {
          nil,
          nil,
        },
        ["null"] = nil,
        ["path"] = "${./testfile}",
        ["str"] = "foo",
        ["true"] = true,
      }
    '';
  };

  luaBindings = shouldPass {
    format = formats.lua {
      asBindings = true;
    };
    input = {
      null = null;
      _false = false;
      _true = true;
      int = 10;
      float = 3.141;
      str = "foo";
      attrs.foo = null;
      list = [
        null
        null
      ];
      path = ./testfile;
      inline = lib.mkLuaInline "hello('world')";
    };
    expected = ''
      _false = false
      _true = true
      attrs = {
        ["foo"] = nil,
      }
      float = 3.141
      inline = (hello("world"))
      int = 10
      list = {
        nil,
        nil,
      }
      null = nil
      path = "${./testfile}"
      str = "foo"
    '';
  };

  nixConfAtoms = shouldPass {
    format = formats.nixConf {
      package = pkgs.nix;
      version = pkgs.nix.version;
      extraOptions = ''ignore-try = false'';
    };
    input = {
      auto-optimise-store = true;
      cores = 0;
      store = "auto";
    };
    # note that null type is hard to test here,
    # as it involves a trailing space our formatter will remove here
    expected = ''
      # WARNING: this file is generated from the nix.* options in
      # your NixOS configuration, typically
      # /etc/nixos/configuration.nix.  Do not edit it!
      auto-optimise-store = true
      cores = 0
      store = auto

      ignore-try = false
    '';
  };

  phpAtoms = shouldPass rec {
    format = formats.php { finalVariable = "config"; };
    input = {
      null = null;
      false = false;
      true = true;
      int = 10;
      float = 3.141;
      str = "foo";
      str_special = "foo\ntesthello'''";
      attrs.foo = null;
      list = [
        null
        null
      ];
      mixed = format.lib.mkMixedArray [ 10 3.141 ] {
        str = "foo";
        attrs.foo = null;
      };
      raw = format.lib.mkRaw "random_function()";
    };
    expected = ''
      <?php
      declare(strict_types=1);
      $config = ['attrs' => ['foo' => null], 'false' => false, 'float' => 3.141000, 'int' => 10, 'list' => [null, null], 'mixed' => [10, 3.141000, 'attrs' => ['foo' => null], 'str' => 'foo'], 'null' => null, 'raw' => random_function(), 'str' => 'foo', 'str_special' => 'foo
      testhello\'\'\'${"'"}, 'true' => true];
    '';
  };

  pythonVars = shouldPass (
    let
      format = formats.pythonVars { };
    in
    {
      inherit format;
      input = {
        _imports = [
          "re"
          "a.b.c"
        ];

        int = 10;
        float = 3.141;
        bool = true;
        str = "foo";
        str_special = "foo\ntesthello'''";
        null = null;
        list = [
          null
          1
          "str"
          true
          (format.lib.mkRaw "1 if True else 2")
        ];
        attrs = {
          foo = null;
          conditional = format.lib.mkRaw "1 if True else 2";
        };
        func = format.lib.mkRaw "re.findall(r'\\bf[a-z]*', 'which foot or hand fell fastest')";
      };
      expected = ''
        import re
        import a.b.c

        attrs = {
            "conditional": 1 if True else 2,
            "foo": None,
        }
        bool = True
        float = 3.141
        func = re.findall(r"\bf[a-z]*", "which foot or hand fell fastest")
        int = 10
        list = [
            None,
            1,
            "str",
            True,
            1 if True else 2,
        ]
        null = None
        str = "foo"
        str_special = "foo\ntesthello''''"
      '';
    }
  );

  phpReturn = shouldPass {
    format = formats.php { };
    input = {
      int = 10;
      float = 3.141;
      str = "foo";
      str_special = "foo\ntesthello'''";
      attrs.foo = null;
    };
    expected = ''
      <?php
      declare(strict_types=1);
      return ['attrs' => ['foo' => null], 'float' => 3.141000, 'int' => 10, 'str' => 'foo', 'str_special' => 'foo
      testhello\'\'\'${"'"}];
    '';
  };

  badgerfishToXmlGenerate = shouldPass {
    format = formats.xml { };
    input = {
      root = {
        "@id" = "123";
        "@class" = "example";
        child1 = {
          "@name" = "child1Name";
          "#text" = "text node";
        };
        child2 = {
          grandchild = "This is a grandchild text node.";
        };
        nulltest = null;
      };
    };
    expected = ''
      <?xml version="1.0" encoding="utf-8"?>
      <root class="example" id="123">
        <child1 name="child1Name">text node</child1>
        <child2>
          <grandchild>This is a grandchild text node.</grandchild>
        </child2>
        <nulltest></nulltest>
      </root>
    '';
  };

  PlistGenerate = shouldPass {
    format = formats.plist { };
    input = {
      null = null;
      false = false;
      true = true;
      int = 10;
      float = 3.141;
      str = "foo";
      attrs.foo = 0;
      list = [
        1
        "hello"
        {
          attrs = {
            key = {
              value = [
                [
                  1
                  2
                  3
                ]
                "test"
              ];
            };
          };
        }
      ];
    };
    expected = ''
      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
      <plist version="1.0">
      <dict>
      ''\t<key>attrs</key>
      ''\t<dict>
      ''\t''\t<key>foo</key>
      ''\t''\t<integer>0</integer>
      ''\t</dict>
      ''\t<key>false</key>
      ''\t<false/>
      ''\t<key>float</key>
      ''\t<real>3.141000</real>
      ''\t<key>int</key>
      ''\t<integer>10</integer>
      ''\t<key>list</key>
      ''\t<array>
      ''\t''\t<integer>1</integer>
      ''\t''\t<string>hello</string>
      ''\t''\t<dict>
      ''\t''\t''\t<key>attrs</key>
      ''\t''\t''\t<dict>
      ''\t''\t''\t''\t<key>key</key>
      ''\t''\t''\t''\t<dict>
      ''\t''\t''\t''\t''\t<key>value</key>
      ''\t''\t''\t''\t''\t<array>
      ''\t''\t''\t''\t''\t''\t<array>
      ''\t''\t''\t''\t''\t''\t''\t<integer>1</integer>
      ''\t''\t''\t''\t''\t''\t''\t<integer>2</integer>
      ''\t''\t''\t''\t''\t''\t''\t<integer>3</integer>
      ''\t''\t''\t''\t''\t''\t</array>
      ''\t''\t''\t''\t''\t''\t<string>test</string>
      ''\t''\t''\t''\t''\t</array>
      ''\t''\t''\t''\t</dict>
      ''\t''\t''\t</dict>
      ''\t''\t</dict>
      ''\t</array>
      ''\t<key>str</key>
      ''\t<string>foo</string>
      ''\t<key>true</key>
      ''\t<true/>
      </dict>
      </plist>'';
  };
}
